package top.codedance.iotp.client.manage;

import top.codedance.iotp.client.manage.net.ComServer;
import top.codedance.iotp.client.manage.net.Manage;
import top.codedance.iotp.client.manage.net.com232.ClientRunner;
import top.codedance.iotp.client.manage.net.configuration.CliCmdOptionsConfig;
import top.codedance.iotp.client.manage.net.configuration.ExecuterRegistConfig;
import top.codedance.iotp.client.manage.net.handler.entity.TaskExecuterRegistEntity;
import top.codedance.iotp.client.manage.net.listenner.TaskExecuter;
import top.codedance.iotp.common.entity.CliCmdEntity;
import top.codedance.iotp.common.entity.CommandTeam;
import top.codedance.iotp.common.entity.NetworkEntity;
import top.codedance.iotp.common.util.*;
import top.codedance.iotp.loadblance.IotClientLoadBlance;
import top.codedance.iotp.loadblance.common.AlgorithmType;
import top.codedance.iotp.loadblance.common.CAlgorithmType;
import top.codedance.iotp.loadblance.entiy.RegistyUri;
import com.fazecast.jSerialComm.SerialPort;
import com.sshtools.forker.client.ForkerBuilder;
import com.sshtools.forker.client.ForkerProcess;
import com.sshtools.forker.common.IO;
import com.sshtools.forker.common.OS;
import com.sshtools.forker.wrapper.ForkerWrapper;
import com.sshtools.forker.wrapper.WrapperIO;
import com.sshtools.forker.wrapper.WrapperProcessFactory;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class ManageApplication {

    private static final String JAVA_HOME = SystemUtil.getJaveHome();
    public static CommandCli commandCli; //命令解析工具
    public static String DEVICE_ID = ""; //设备唯一编号
    public static volatile AsymmetricEncryption.KeyStringPair keyStringPair; //通讯密钥对
    public static volatile String serverPublicKey; //服务端公钥
    public static Map<CommandTeam, TaskExecuter> excuters = new HashMap<>(); //同步执行器插件集合
    public static ExecutorService pool; //线程池管理
    public static ExecutorService workerPool = Executors.newCachedThreadPool(); //同步任务线程池
    public static VersionFileUtil versionFileUtil; //版本操作工具
    public static volatile long preClientVersionCode = 0; //客户端当前的版本号
    public static volatile long lastClientVersionCode = Long.valueOf(Version.VERSION); //客户端当前的版本号
    public static boolean isAutoUpdate = true; //是否启动自动更新
    public static double cpuRate = 60; //cpu使用率
    private static String[] argumengs;
    private static boolean upgrading = false; //是否在更新
    public static volatile short loadRequestCount = 0; //请求客户端负载的次数，每收到一次回复减1
    public static ForkerProcess p = null;
    public static ForkerProcess p2 = null;
    public static Future<Object> clientFuture; //客户端Future
    private static String clientJarFilePath;
    private static File easyMediaJarFile;
    public static boolean enableLoadBlance = false; //是否开启分布式，如开启将使用注册中心来分配注册到哪个server，手动指定server IP 无效
    public static int lbType = 0; //负载均衡类型
    public static String[] registryHosts = new String[]{"http://127.0.0.1:10014"}; //注册中心的地址
    private static String finalIp;
    private static int finalPort;
    private static int baudRate = 9600;
    public static ClientRunner clientRunner = null;
    private static final List<String> JVMParams = new ArrayList<>();
    public static int ioThreads = Runtime.getRuntime().availableProcessors() * 2;
    public static int workerThreads = ioThreads * 4;
    private static boolean enableManage = false;

    private static Logger logger;

    public static void main(String[] args) throws Exception {
        if (JAVA_HOME.isEmpty()) {
            System.out.println("Please setting env JAVA_HOME.");
            System.exit(0);
        }
        RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean();
        List<String> aList = bean.getInputArguments();
        JVMParams.addAll(aList);

        argumengs = args;
        System.setProperty("java.net.preferIPv4Stack", "true");
        System.setProperty("io.netty.noPreferDirect", "true");
        System.setProperty("io.netty.tmpdir", OSinfo.isWindows() ? System.getProperty("java.io.tmpdir") : "/var/tmp");
        commandCli = new CommandCli(args, CliCmdOptionsConfig.getCmds());
        logger = getLogger(ManageApplication.class);
        logger.setLevel(Level.toLevel(ManageApplication.commandCli.getArg("logLevel", "ERROR")));

        for (TaskExecuterRegistEntity taskExecuterRegistEntity : ExecuterRegistConfig.getSyscExecuters()) {
            registerSyncExecuter(taskExecuterRegistEntity.getCommandTeam(), taskExecuterRegistEntity.getTaskExecuter());
        }

        DEVICE_ID = commandCli.getArg("deviceId", "testDeviceId");
        isAutoUpdate = commandCli.getArg("autoUpdate", false);
        cpuRate = commandCli.getArg("cpu", 60);
        enableLoadBlance = commandCli.getArg("enableLoadBlance", false);
        lbType = commandCli.getArg("lbType", 0);
        registryHosts = commandCli.getArg("registryHosts", registryHosts[0]).split("\\|");
        baudRate = commandCli.getArg("baudRate", 9600);
        enableManage = commandCli.getArg("enableManage", false);
        ioThreads = commandCli.getArg("ioThreads", Runtime.getRuntime().availableProcessors() * 2);
        workerThreads = commandCli.getArg("workerThreads", ioThreads * 4);

        versionFileUtil = new VersionFileUtil(ManageApplication.class);
        long version = Long.valueOf(versionFileUtil.get());
        if (version >= lastClientVersionCode) {
            lastClientVersionCode = version;
        } else {
            versionFileUtil.update(String.valueOf(lastClientVersionCode));
        }

        updateClientJar();
        getEasyMediaJar();
        intPool();

        run();
    }

    private static void intPool(){
        int executorCount = 1;
        if(enableManage){
            executorCount++;
        }
        if(easyMediaJarFile.exists()){
            executorCount++;
        }
        runLB();
        if(!finalIp.trim().isEmpty() && finalPort > 0){
            executorCount++;
        }
        pool = Executors.newFixedThreadPool(executorCount);
    }

    public static void run() {
        if (enableManage && SerialPort.getCommPorts().length > 0) {
            pool.submit(() -> {
                while (true) {
                    try {
                        new ComServer(baudRate);
                    } catch (Exception ex) {
                        logger.debug(ex.getMessage());
                    }
                    try {
                        Thread.sleep(2000);
                        ManageApplication.syslogInfo("ComServer reconnecting ... ");
                    } catch (Exception ex) {
                    }
                }
            });
        }

        if(!finalIp.trim().isEmpty() && finalPort > 0) {
            pool.submit(() -> {
                while (true) {
                    try {
                        new Manage(finalIp, finalPort).run();
                    } catch (Exception ex) {
                        logger.debug(ex.getMessage());
                        ManageApplication.syslogInfo("Manage connect fail!");
                    }
                    try {
                        Thread.sleep(2000);
                        ManageApplication.syslogInfo("Manage reconnecting ... ");
                        runLB();
                    } catch (Exception ex) {
                    }
                }
            });
        }

        clientFuture = pool.submit(() -> {
            destoryClients();
            while (true) {
                if (!upgrading) {
                    Future<?> runnerFuture = null;
                    try {
                        updateClientJar();
                        File clientJarFile = new File(clientJarFilePath);
                        if (clientJarFile.exists()) {
                            ForkerBuilder fb = new ForkerBuilder();
                            fb.directory(new File(SystemUtil.getRuntimeDirPath(ManageApplication.class)));
                            List<String> tmpArgs = new ArrayList<>();
                            tmpArgs.add(OS.getJavaPath());
                            for (int i = 0; i < JVMParams.size(); i++) {
                                if(JVMParams.get(i).startsWith("-agentlib") || JVMParams.get(i).startsWith("-javaagent")){
                                    continue;
                                }
                                tmpArgs.add(JVMParams.get(i));
                            }
                            tmpArgs.add("-jar");
                            tmpArgs.add(clientJarFile.getAbsolutePath());
                            List<String> exludeCmds = CliCmdOptionsConfig.getExludeCmds();
                            String[] split;
                            String replace;
                            boolean isSkip = false;
                            for (String v : argumengs) {
                                if(isSkip){
                                    isSkip = false;
                                    continue;
                                }
                                split = v.split("=");
                                replace = split[0].replace("--", "").replace("-", "");
                                if (!exludeCmds.contains(replace)) {
                                    tmpArgs.add(v);
                                }else{
                                    isSkip = CliCmdOptionsConfig.hasArgs(replace);
                                }
                            }
                            fb.command(tmpArgs);
                            logger.debug(tmpArgs);
                            WrapperProcessFactory processFactory = fb.configuration().processFactory(WrapperProcessFactory.class);
                            processFactory.addOption(new ForkerWrapper.KeyValuePair("native", "true"));
                            processFactory.addOption(new ForkerWrapper.KeyValuePair("level", "ALL"));
                            fb.io(WrapperIO.WRAPPER);
                            fb.redirectErrorStream(true);
                            p = fb.start();
                            clientRunner = new ClientRunner(p);
                            runnerFuture = workerPool.submit(clientRunner);
                            logger.debug(p.waitFor());
                            clientRunner.stop();
                        } else {
                            setVersion(new Long(0)); //把版本置成最低版本，可以诱发升级程序，从面实现下载最新版本的客户端。
                            try {
                                Thread.sleep(5000);
                            } catch (Exception ex) {
                            }
                        }
                    } catch (Exception ex) {
                        logger.debug(ex.getMessage());
                        ManageApplication.syslogInfo("Client exited!");
                    } finally {
                        if (p != null) {
                            p.destroyForcibly();
                            p = null;
                        }
                        if(runnerFuture != null) {
                            runnerFuture.cancel(true);
                        }
                        clientRunner = null;
                    }
                }

                while (upgrading) {
                    try {
                        Thread.sleep(2000);
                    } catch (Exception ex) {
                    }
                }
                ManageApplication.syslogInfo("Manage restart client ... ");

            }
        });

        if (easyMediaJarFile.exists()) {
            pool.submit(() -> {
                while (true) {
                    try {
                        ForkerBuilder fb = new ForkerBuilder();
                        fb.directory(new File(SystemUtil.getRuntimeDirPath(ManageApplication.class)));
                        List<String> tmpArgs = new ArrayList<>();
                        tmpArgs.add(OS.getJavaPath());
                        tmpArgs.add("-jar");
                        tmpArgs.add(easyMediaJarFile.getAbsolutePath());
                        fb.command(tmpArgs);
                        logger.debug(tmpArgs);
                        WrapperProcessFactory processFactory = fb.configuration().processFactory(WrapperProcessFactory.class);
                        processFactory.addOption(new ForkerWrapper.KeyValuePair("native", "true"));
                        processFactory.addOption(new ForkerWrapper.KeyValuePair("level", "ALL"));
                        fb.io(WrapperIO.WRAPPER);
                        fb.redirectErrorStream(true);
                        p2 = fb.start();
                        IOUtils.copy(p2.getInputStream(), System.out);
                        logger.debug(p2.waitFor());
                    } catch (Exception ex) {
                        logger.debug(ex.getMessage());
                        ManageApplication.syslogInfo("Client exited!");
                    } finally {
                        if (p2 != null) {
                            p2.destroyForcibly();
                            p2 = null;
                        }
                    }

                    try {
                        Thread.sleep(2000);
                    } catch (Exception ex) {
                    }
                    ManageApplication.syslogInfo("Manage restart mediaServer ... ");
                }
            });
        }

        try {
            pool.awaitTermination(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            logger.info("thread pool finished, and restart task.");
        } finally {
            if (pool.isShutdown()) {
                intPool();
                try {
                    Thread.sleep(2000);
                    ManageApplication.syslogInfo("Manage restarting ... ");
                } catch (Exception ex) {
                }
                run();
            }
        }
    }

    private static String runCmd(String cmd) {
        ByteArrayOutputStream baout = new ByteArrayOutputStream();
        ForkerBuilder builder;
        Process p = null;
        try {
            builder = new ForkerBuilder().io(IO.IO);
            builder.redirectErrorStream(true).io(IO.IO).command("sh", "-c", cmd);
            p = builder.start();
            IOUtils.copy(p.getInputStream(), baout);
            p.waitFor(3, TimeUnit.SECONDS);
            return new String(baout.toByteArray());
        } catch (Exception ex) {
            return null;
        } finally {
            if (p != null) {
                p.destroyForcibly();
            }
        }
    }

    /**
     * linux下结束由于强制结束进程导致的子进程没有关闭问题。
     */
    private static void destoryClients() {
        if (SystemUtils.IS_OS_LINUX | SystemUtils.IS_OS_MAC) {
            String result = runCmd("ps -eo pid,cmd --sort -pid,-cmd|grep -E \"client-[0-9]\\{1,\\}\\.jar\"|grep -v grep|awk '{print $1}'");
            if(SystemUtils.IS_OS_MAC) {
                result = runCmd("ps -e -o pid,command|grep -E \"client-[0-9]\\{1,\\}\\.jar\"|grep -v grep|awk '{print $1}'");
            }
            if (result != null && !result.isEmpty()) {
                String[] pids = result.split("\\n");
                for (String pid : pids) {
                    runCmd("kill -9 " + pid);
                }
            }
        }
    }

    /**
     * 执行负载均衡
     */
    private static synchronized void runLB() {
        if (enableLoadBlance) {
            NetworkEntity networkEntity = null;
            int j = 0;
            while (j < 3) {
                try {
                    networkEntity = loadBlance();
                    break;
                } catch (Exception e) {
                    j++;
                    ManageApplication.syslogInfo("Load blance fail, restarting ... ");
                    try {
                        Thread.sleep(2000);
                    } catch (Exception ex) {
                    }
                }
            }
            if (networkEntity != null) {
                finalIp = networkEntity.getIp();
                finalPort = networkEntity.getPort();
                List<String> tmpArgs = new ArrayList<>();
                List<CliCmdEntity> cmds = CliCmdOptionsConfig.getCmds();
                for (int i = 0; i < cmds.size(); i++) {
                    CliCmdEntity cce = cmds.get(i);
                    if (!cce.isJoin()) {
                        continue;
                    }
                    if (!commandCli.hasArg(cce.getLongOpt())) {
                        continue;
                    }
                    if (cmds.get(i).getLongOpt().equals("host")) {
                        tmpArgs.add("--" + cmds.get(i).getLongOpt() + "=" + finalIp);
                        continue;
                    } else if (cmds.get(i).getLongOpt().equals("port")) {
                        tmpArgs.add("--" + cmds.get(i).getLongOpt() + "=" + finalPort);
                        continue;
                    } else {
                        if (cmds.get(i).isHasArg()) {
                            tmpArgs.add("--" + cmds.get(i).getLongOpt() + "=" + commandCli.getArg(cmds.get(i).getLongOpt()));
                        } else {
                            tmpArgs.add("--" + cmds.get(i).getLongOpt());
                        }
                    }
                }
                String[] endTmpArgs = new String[tmpArgs.size()];
                tmpArgs.toArray(endTmpArgs);
                argumengs = endTmpArgs;
            } else {
                syslogInfo("Load blance fail, please turn off load balancing and try again or contact the administrator.");
                finalIp = commandCli.getArg("host", "");
                finalPort = commandCli.getArg("port", 0);
            }
        } else {
            finalIp = commandCli.getArg("host", "");
            finalPort = commandCli.getArg("port", 0);
        }
    }

    /**
     * 注册同步执行器
     *
     * @param commandTeam
     * @param taskExecuter
     */
    private static void registerSyncExecuter(CommandTeam commandTeam, TaskExecuter taskExecuter) {
        excuters.put(commandTeam, taskExecuter);
    }

    /**
     * 获取log对象并动态设置日志级别
     *
     * @param clazz
     * @return
     */
    public static Logger getLogger(Class<?> clazz) {
        Logger logger = Logger.getLogger(clazz);
        logger.setLevel(Level.toLevel(ManageApplication.commandCli.getArg("logLevel", "ERROR")));
        return logger;
    }

    /**
     * 系统日志输出，任何情况下都打印
     *
     * @param info
     */
    public static void syslogInfo(String info) {
        System.out.println(info);
    }

    public static AsymmetricEncryption.KeyStringPair getKeyStringPair() {
        return keyStringPair;
    }

    public static void setKeyStringPair(AsymmetricEncryption.KeyStringPair keyStringPair) {
        ManageApplication.keyStringPair = keyStringPair;
    }

    public static String getServerPublicKey() {
        return serverPublicKey;
    }

    public static void setServerPublicKey(String serverPublicKey) {
        ManageApplication.serverPublicKey = serverPublicKey;
    }

    public static void startUpgrade() {
        upgrading = true;
    }

    public static void endUpgrade() {
        upgrading = false;
        if (p != null) {
            p.destroyForcibly();
        }
        ManageApplication.clientFuture.cancel(true);
    }

    public static boolean isUpgrading() {
        return upgrading;
    }

    public static Long getVersion() {
        return lastClientVersionCode;
    }

    public static void setVersion(Long version) {
        preClientVersionCode = lastClientVersionCode;
        lastClientVersionCode = version;
        try {
            versionFileUtil.update(version.toString());
        } catch (Exception e) {
            syslogInfo("Save version fail.");
        }
    }

    public static void updateClientJar() {
        clientJarFilePath = SystemUtil.getRuntimeDirPath(ManageApplication.class) + "client-" + lastClientVersionCode + ".jar";
    }

    public static void getEasyMediaJar() {
        easyMediaJarFile = new File(SystemUtil.getRuntimeDirPath(ManageApplication.class) + "EasyMedia.jar");
    }

    /**
     * 注册时负载均衡
     *
     * @return
     */
    public static NetworkEntity loadBlance() throws Exception {
        NetworkEntity ne = null;
        List<RegistyUri> registyUris = new ArrayList<>();
        for (String registryHost : registryHosts) {
            RegistyUri registyUri1 = new RegistyUri(registryHost, 0);
            registyUris.add(registyUri1);
        }
        CAlgorithmType cat;
        switch (lbType) {
            case 2:
                cat = CAlgorithmType.CYCLE;
                break;
            case 3:
                cat = CAlgorithmType.RANDOM;
                break;
            case 4:
                cat = CAlgorithmType.WEIGHT;
                break;
            case 1:
            default:
                cat = CAlgorithmType.AUTO;
        }
        IotClientLoadBlance iotClientLoadBlance = IotClientLoadBlance.getInstance(registyUris, AlgorithmType.AUTO, cat);
        String serverURI = iotClientLoadBlance.blanceHttp(DEVICE_ID);
        String[] splits = serverURI.split("\\:");
        if (splits.length > 1) {
            ne = new NetworkEntity(splits[0], Integer.parseInt(splits[1]));
        }
        return ne;
    }
}
